Skip to content

Bucket level multi-tenant trigger#2098

Merged
eirsep merged 3 commits intoopensearch-project:mainfrom
eirsep:bucket-level-multi-tenant-trigger
Apr 29, 2026
Merged

Bucket level multi-tenant trigger#2098
eirsep merged 3 commits intoopensearch-project:mainfrom
eirsep:bucket-level-multi-tenant-trigger

Conversation

@eirsep
Copy link
Copy Markdown
Member

@eirsep eirsep commented Apr 17, 2026

Replace the custom BucketSelectorExt aggregation with standard
bucket_selector for bucket-level trigger evaluation when
multi_tenant_trigger_eval_enabled is true. This enables trigger
evaluation on user clusters that do not have the alerting plugin
installed.

When the flag is on, bucket-level monitors are limited to 1 trigger.
The standard bucket_selector is injected directly into the monitor's
search query so a single search call performs both data collection and
trigger evaluation. Include/exclude filtering from BucketSelectorExtFilter
is replicated post-response by BucketKeyFilter.

The existing BucketSelectorExt path is completely unchanged when the
flag is off (the default).

* Extracts include/exclude regex patterns from [IncludeExclude].
* Uses reflection to access private fields since no public API exposes the raw patterns.
*/
private fun extractPatterns(includeExclude: IncludeExclude): Pair<Pattern?, Pattern?> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reflection is an anti-pattern and won't be supported in some environments. Is there a way to take a dependency on the IncludeExclude object or recreate it in a way that we can access the fields without reflection?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to parse the json

}
}
if (found == null) {
throw IllegalArgumentException("ParentBucketPath: $parentBucketPath not found in query aggregations")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing the exception here would only allow one iteration of the for loop right? I'm wondering if that's the intended behavior or if we are looking for the parentAgg in any of the available pathElements

Copy link
Copy Markdown
Member Author

@eirsep eirsep Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two nested loops:

  • Outer loop: iterates over pathElements (e.g., for agg1>agg2, it walks agg1 then agg2)
  • Inner loop: searches the current level's agg builders for the matching name

The throw is inside the outer loop but outside the inner loop. If agg1 is found, the outer loop continues to look for agg2 inside agg1's sub-aggregations. The exception only fires if a path element isn't found at its expected level — which is correct behavior imo.

* since the field is private with no public getter in the commons library.
*/
@Suppress("UNCHECKED_CAST")
private fun extractBucketsPathsMap(selector: BucketSelectorExtAggregationBuilder): Map<String, String> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment on reflection

}
}

private fun isIndexNotFoundException(e: Throwable): Boolean {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this from another PR? It looks familiar and doesn't appear to be used anywhere

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread alerting/src/main/kotlin/org/opensearch/alerting/TriggerService.kt Outdated
eirsep added 3 commits April 28, 2026 19:17
…ard bucket_selector

Replace the custom BucketSelectorExt aggregation with standard
bucket_selector for bucket-level trigger evaluation when
multi_tenant_trigger_eval_enabled is true. This enables trigger
evaluation on user clusters that do not have the alerting plugin
installed.

When the flag is on, bucket-level monitors are limited to 1 trigger.
The standard bucket_selector is injected directly into the monitor's
search query so a single search call performs both data collection and
trigger evaluation. Include/exclude filtering from BucketSelectorExtFilter
is replicated post-response by BucketKeyFilter.

The existing BucketSelectorExt path is completely unchanged when the
flag is off (the default).

New files:
- BucketSelectorQueryBuilder: injects standard bucket_selector sub-agg
- BucketKeyFilter: post-response include/exclude bucket filtering
- TriggerService.runBucketLevelTriggerFromFilteredResponse(): reads
  remaining buckets from filtered response

Modified files:
- BucketLevelMonitorRunner: flag-gated single-query path
- InputService: useStandardBucketSelector parameter
- TransportIndexMonitorAction: 1-trigger validation for bucket-level
  monitors when flag is on
- AggregationQueryRewriter: skipBucketSelectorInjection parameter

Integration tests:
- RemoteBucketLevelTriggerIT: 13 tests covering composite/terms agg,
  alert lifecycle, dry run, nested parent path, include/exclude filter,
  validation of 1-trigger limit, Painless script in query
- RemoteBucketLevelTriggerRegressionIT: 5 tests verifying flag=false
  preserves existing BucketSelectorExt behavior

Signed-off-by: Surya Sashank Nistala <snistala@amazon.com>
- Remove collectInputResultsForTrigger() from InputService (dead code
  from per-trigger query approach)
- Remove skipAllBucketSelectorInjection parameter from collectInputResults
  and getSearchRequest (no longer used)
- Collapse identical triggerCtx if/else branches in BucketLevelMonitorRunner
- Restore .gitignore to original state

Signed-off-by: Surya Sashank Nistala <snistala@amazon.com>
…dation

- BucketSelectorQueryBuilder: use public bucketsPathsMap getter from
  common-utils instead of reflection (opensearch-project/common-utils#949)
- BucketKeyFilter: use IncludeExclude.convertToStringFilter() instead
  of reflection on private fields
- TriggerService: replace blanket @Suppress(UNCHECKED_CAST) with
  runtime type checks using require()
- BucketLevelMonitorRunner: add 1-trigger validation at execution time
  to cover the _execute API path (in addition to create/update validation)

Signed-off-by: Surya Sashank Nistala <snistala@amazon.com>
@eirsep eirsep force-pushed the bucket-level-multi-tenant-trigger branch from d82f8ad to 31f8028 Compare April 29, 2026 19:04
@eirsep eirsep merged commit a1e869c into opensearch-project:main Apr 29, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants